// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Binary to decimal floating point conversion.
// Algorithm:
//   1) store mantissa in multiprecision decimal
//   2) shift decimal by exponent
//   3) read digits out & format

// package strconv -- go2cs converted at 2022 March 13 05:41:22 UTC
// import "strconv" ==> using strconv = go.strconv_package
// Original source: C:\Program Files\Go\src\strconv\ftoa.go
namespace go;

using math = math_package;

public static partial class strconv_package {

// TODO: move elsewhere?
private partial struct floatInfo {
    public nuint mantbits;
    public nuint expbits;
    public nint bias;
}

private static floatInfo float32info = new floatInfo(23,8,-127);
private static floatInfo float64info = new floatInfo(52,11,-1023);

// FormatFloat converts the floating-point number f to a string,
// according to the format fmt and precision prec. It rounds the
// result assuming that the original was obtained from a floating-point
// value of bitSize bits (32 for float32, 64 for float64).
//
// The format fmt is one of
// 'b' (-ddddp±ddd, a binary exponent),
// 'e' (-d.dddde±dd, a decimal exponent),
// 'E' (-d.ddddE±dd, a decimal exponent),
// 'f' (-ddd.dddd, no exponent),
// 'g' ('e' for large exponents, 'f' otherwise),
// 'G' ('E' for large exponents, 'f' otherwise),
// 'x' (-0xd.ddddp±ddd, a hexadecimal fraction and binary exponent), or
// 'X' (-0Xd.ddddP±ddd, a hexadecimal fraction and binary exponent).
//
// The precision prec controls the number of digits (excluding the exponent)
// printed by the 'e', 'E', 'f', 'g', 'G', 'x', and 'X' formats.
// For 'e', 'E', 'f', 'x', and 'X', it is the number of digits after the decimal point.
// For 'g' and 'G' it is the maximum number of significant digits (trailing
// zeros are removed).
// The special precision -1 uses the smallest number of digits
// necessary such that ParseFloat will return f exactly.
public static @string FormatFloat(double f, byte fmt, nint prec, nint bitSize) {
    return string(genericFtoa(make_slice<byte>(0, max(prec + 4, 24)), f, fmt, prec, bitSize));
}

// AppendFloat appends the string form of the floating-point number f,
// as generated by FormatFloat, to dst and returns the extended buffer.
public static slice<byte> AppendFloat(slice<byte> dst, double f, byte fmt, nint prec, nint bitSize) {
    return genericFtoa(dst, f, fmt, prec, bitSize);
}

private static slice<byte> genericFtoa(slice<byte> dst, double val, byte fmt, nint prec, nint bitSize) => func((_, panic, _) => {
    ulong bits = default;
    ptr<floatInfo> flt;
    switch (bitSize) {
        case 32: 
            bits = uint64(math.Float32bits(float32(val)));
            flt = _addr_float32info;
            break;
        case 64: 
            bits = math.Float64bits(val);
            flt = _addr_float64info;
            break;
        default: 
            panic("strconv: illegal AppendFloat/FormatFloat bitSize");
            break;
    }

    var neg = bits >> (int)((flt.expbits + flt.mantbits)) != 0;
    var exp = int(bits >> (int)(flt.mantbits)) & (1 << (int)(flt.expbits) - 1);
    var mant = bits & (uint64(1) << (int)(flt.mantbits) - 1);

    switch (exp) {
        case 1 << (int)(flt.expbits) - 1: 
            // Inf, NaN
            @string s = default;

            if (mant != 0) 
                s = "NaN";
            else if (neg) 
                s = "-Inf";
            else 
                s = "+Inf";
                    return append(dst, s);
            break;
        case 0: 
            // denormalized
            exp++;
            break;
        default: 
            // add implicit top bit
            mant |= uint64(1) << (int)(flt.mantbits);
            break;
    }
    exp += flt.bias; 

    // Pick off easy binary, hex formats.
    if (fmt == 'b') {
        return fmtB(dst, neg, mant, exp, flt);
    }
    if (fmt == 'x' || fmt == 'X') {
        return fmtX(dst, prec, fmt, neg, mant, exp, flt);
    }
    if (!optimize) {
        return bigFtoa(dst, prec, fmt, neg, mant, exp, flt);
    }
    ref decimalSlice digs = ref heap(out ptr<decimalSlice> _addr_digs);
    var ok = false; 
    // Negative precision means "only as much as needed to be exact."
    var shortest = prec < 0;
    if (shortest) { 
        // Use Ryu algorithm.
        array<byte> buf = new array<byte>(32);
        digs.d = buf[..];
        ryuFtoaShortest(_addr_digs, mant, exp - int(flt.mantbits), flt);
        ok = true; 
        // Precision for shortest representation mode.
        switch (fmt) {
            case 'e': 

            case 'E': 
                prec = max(digs.nd - 1, 0);
                break;
            case 'f': 
                prec = max(digs.nd - digs.dp, 0);
                break;
            case 'g': 

            case 'G': 
                prec = digs.nd;
                break;
        }
    }
    else if (fmt != 'f') { 
        // Fixed number of digits.
        var digits = prec;
        switch (fmt) {
            case 'e': 

            case 'E': 
                digits++;
                break;
            case 'g': 

            case 'G': 
                if (prec == 0) {
                    prec = 1;
                }
                digits = prec;
                break;
        }
        buf = new array<byte>(24);
        if (bitSize == 32 && digits <= 9) {
            digs.d = buf[..];
            ryuFtoaFixed32(_addr_digs, uint32(mant), exp - int(flt.mantbits), digits);
            ok = true;
        }
        else if (digits <= 18) {
            digs.d = buf[..];
            ryuFtoaFixed64(_addr_digs, mant, exp - int(flt.mantbits), digits);
            ok = true;
        }
    }
    if (!ok) {
        return bigFtoa(dst, prec, fmt, neg, mant, exp, flt);
    }
    return formatDigits(dst, shortest, neg, digs, prec, fmt);
});

// bigFtoa uses multiprecision computations to format a float.
private static slice<byte> bigFtoa(slice<byte> dst, nint prec, byte fmt, bool neg, ulong mant, nint exp, ptr<floatInfo> _addr_flt) {
    ref floatInfo flt = ref _addr_flt.val;

    ptr<object> d = @new<decimal>();
    d.Assign(mant);
    d.Shift(exp - int(flt.mantbits));
    decimalSlice digs = default;
    var shortest = prec < 0;
    if (shortest) {
        roundShortest(d, mant, exp, _addr_flt);
        digs = new decimalSlice(d:d.d[:],nd:d.nd,dp:d.dp); 
        // Precision for shortest representation mode.
        switch (fmt) {
            case 'e': 

            case 'E': 
                prec = digs.nd - 1;
                break;
            case 'f': 
                prec = max(digs.nd - digs.dp, 0);
                break;
            case 'g': 

            case 'G': 
                prec = digs.nd;
                break;
        }
    }
    else
 { 
        // Round appropriately.
        switch (fmt) {
            case 'e': 

            case 'E': 
                d.Round(prec + 1);
                break;
            case 'f': 
                d.Round(d.dp + prec);
                break;
            case 'g': 

            case 'G': 
                if (prec == 0) {
                    prec = 1;
                }
                d.Round(prec);
                break;
        }
        digs = new decimalSlice(d:d.d[:],nd:d.nd,dp:d.dp);
    }
    return formatDigits(dst, shortest, neg, digs, prec, fmt);
}

private static slice<byte> formatDigits(slice<byte> dst, bool shortest, bool neg, decimalSlice digs, nint prec, byte fmt) {
    switch (fmt) {
        case 'e': 

        case 'E': 
            return fmtE(dst, neg, digs, prec, fmt);
            break;
        case 'f': 
            return fmtF(dst, neg, digs, prec);
            break;
        case 'g': 
            // trailing fractional zeros in 'e' form will be trimmed.

        case 'G': 
            // trailing fractional zeros in 'e' form will be trimmed.
            var eprec = prec;
            if (eprec > digs.nd && digs.nd >= digs.dp) {
                eprec = digs.nd;
            }
            if (shortest) {
                eprec = 6;
            }
            var exp = digs.dp - 1;
            if (exp < -4 || exp >= eprec) {
                if (prec > digs.nd) {
                    prec = digs.nd;
                }
                return fmtE(dst, neg, digs, prec - 1, fmt + 'e' - 'g');
            }
            if (prec > digs.dp) {
                prec = digs.nd;
            }
            return fmtF(dst, neg, digs, max(prec - digs.dp, 0));
            break;
    } 

    // unknown format
    return append(dst, '%', fmt);
}

// roundShortest rounds d (= mant * 2^exp) to the shortest number of digits
// that will let the original floating point value be precisely reconstructed.
private static void roundShortest(ptr<decimal> _addr_d, ulong mant, nint exp, ptr<floatInfo> _addr_flt) {
    ref decimal d = ref _addr_d.val;
    ref floatInfo flt = ref _addr_flt.val;
 
    // If mantissa is zero, the number is zero; stop now.
    if (mant == 0) {
        d.nd = 0;
        return ;
    }
    var minexp = flt.bias + 1; // minimum possible exponent
    if (exp > minexp && 332 * (d.dp - d.nd) >= 100 * (exp - int(flt.mantbits))) { 
        // The number is already shortest.
        return ;
    }
    ptr<decimal> upper = @new<decimal>();
    upper.Assign(mant * 2 + 1);
    upper.Shift(exp - int(flt.mantbits) - 1); 

    // d = mant << (exp - mantbits)
    // Next lowest floating point number is mant-1 << exp-mantbits,
    // unless mant-1 drops the significant bit and exp is not the minimum exp,
    // in which case the next lowest is mant*2-1 << exp-mantbits-1.
    // Either way, call it mantlo << explo-mantbits.
    // Our lower bound is halfway between, mantlo*2+1 << explo-mantbits-1.
    ulong mantlo = default;
    nint explo = default;
    if (mant > 1 << (int)(flt.mantbits) || exp == minexp) {
        mantlo = mant - 1;
        explo = exp;
    }
    else
 {
        mantlo = mant * 2 - 1;
        explo = exp - 1;
    }
    ptr<decimal> lower = @new<decimal>();
    lower.Assign(mantlo * 2 + 1);
    lower.Shift(explo - int(flt.mantbits) - 1); 

    // The upper and lower bounds are possible outputs only if
    // the original mantissa is even, so that IEEE round-to-even
    // would round to the original mantissa and not the neighbors.
    var inclusive = mant % 2 == 0; 

    // As we walk the digits we want to know whether rounding up would fall
    // within the upper bound. This is tracked by upperdelta:
    //
    // If upperdelta == 0, the digits of d and upper are the same so far.
    //
    // If upperdelta == 1, we saw a difference of 1 between d and upper on a
    // previous digit and subsequently only 9s for d and 0s for upper.
    // (Thus rounding up may fall outside the bound, if it is exclusive.)
    //
    // If upperdelta == 2, then the difference is greater than 1
    // and we know that rounding up falls within the bound.
    byte upperdelta = default; 

    // Now we can figure out the minimum number of digits required.
    // Walk along until d has distinguished itself from upper and lower.
    for (nint ui = 0; ; ui++) { 
        // lower, d, and upper may have the decimal points at different
        // places. In this case upper is the longest, so we iterate from
        // ui==0 and start li and mi at (possibly) -1.
        var mi = ui - upper.dp + d.dp;
        if (mi >= d.nd) {
            break;
        }
        var li = ui - upper.dp + lower.dp;
        var l = byte('0'); // lower digit
        if (li >= 0 && li < lower.nd) {
            l = lower.d[li];
        }
        var m = byte('0'); // middle digit
        if (mi >= 0) {
            m = d.d[mi];
        }
        var u = byte('0'); // upper digit
        if (ui < upper.nd) {
            u = upper.d[ui];
        }
        var okdown = l != m || inclusive && li + 1 == lower.nd;


        if (upperdelta == 0 && m + 1 < u) 
            // Example:
            // m = 12345xxx
            // u = 12347xxx
            upperdelta = 2;
        else if (upperdelta == 0 && m != u) 
            // Example:
            // m = 12345xxx
            // u = 12346xxx
            upperdelta = 1;
        else if (upperdelta == 1 && (m != '9' || u != '0')) 
            // Example:
            // m = 1234598x
            // u = 1234600x
            upperdelta = 2;
        // Okay to round up if upper has a different digit and either upper
        // is inclusive or upper is bigger than the result of rounding up.
        var okup = upperdelta > 0 && (inclusive || upperdelta > 1 || ui + 1 < upper.nd); 

        // If it's okay to do either, then round to the nearest one.
        // If it's okay to do only one, do it.

        if (okdown && okup) 
            d.Round(mi + 1);
            return ;
        else if (okdown) 
            d.RoundDown(mi + 1);
            return ;
        else if (okup) 
            d.RoundUp(mi + 1);
            return ;
            }
}

private partial struct decimalSlice {
    public slice<byte> d;
    public nint nd;
    public nint dp;
    public bool neg;
}

// %e: -d.ddddde±dd
private static slice<byte> fmtE(slice<byte> dst, bool neg, decimalSlice d, nint prec, byte fmt) { 
    // sign
    if (neg) {
        dst = append(dst, '-');
    }
    var ch = byte('0');
    if (d.nd != 0) {
        ch = d.d[0];
    }
    dst = append(dst, ch); 

    // .moredigits
    if (prec > 0) {
        dst = append(dst, '.');
        nint i = 1;
        var m = min(d.nd, prec + 1);
        if (i < m) {
            dst = append(dst, d.d[(int)i..(int)m]);
            i = m;
        }
        while (i <= prec) {
            dst = append(dst, '0');
            i++;
        }
    }
    dst = append(dst, fmt);
    var exp = d.dp - 1;
    if (d.nd == 0) { // special case: 0 has exponent 0
        exp = 0;
    }
    if (exp < 0) {
        ch = '-';
        exp = -exp;
    }
    else
 {
        ch = '+';
    }
    dst = append(dst, ch); 

    // dd or ddd

    if (exp < 10) 
        dst = append(dst, '0', byte(exp) + '0');
    else if (exp < 100) 
        dst = append(dst, byte(exp / 10) + '0', byte(exp % 10) + '0');
    else 
        dst = append(dst, byte(exp / 100) + '0', byte(exp / 10) % 10 + '0', byte(exp % 10) + '0');
        return dst;
}

// %f: -ddddddd.ddddd
private static slice<byte> fmtF(slice<byte> dst, bool neg, decimalSlice d, nint prec) { 
    // sign
    if (neg) {
        dst = append(dst, '-');
    }
    if (d.dp > 0) {
        var m = min(d.nd, d.dp);
        dst = append(dst, d.d[..(int)m]);
        while (m < d.dp) {
            dst = append(dst, '0');
            m++;
        }
    else
    } {
        dst = append(dst, '0');
    }
    if (prec > 0) {
        dst = append(dst, '.');
        for (nint i = 0; i < prec; i++) {
            var ch = byte('0');
            {
                var j = d.dp + i;

                if (0 <= j && j < d.nd) {
                    ch = d.d[j];
                }

            }
            dst = append(dst, ch);
        }
    }
    return dst;
}

// %b: -ddddddddp±ddd
private static slice<byte> fmtB(slice<byte> dst, bool neg, ulong mant, nint exp, ptr<floatInfo> _addr_flt) {
    ref floatInfo flt = ref _addr_flt.val;
 
    // sign
    if (neg) {
        dst = append(dst, '-');
    }
    dst, _ = formatBits(dst, mant, 10, false, true); 

    // p
    dst = append(dst, 'p'); 

    // ±exponent
    exp -= int(flt.mantbits);
    if (exp >= 0) {
        dst = append(dst, '+');
    }
    dst, _ = formatBits(dst, uint64(exp), 10, exp < 0, true);

    return dst;
}

// %x: -0x1.yyyyyyyyp±ddd or -0x0p+0. (y is hex digit, d is decimal digit)
private static slice<byte> fmtX(slice<byte> dst, nint prec, byte fmt, bool neg, ulong mant, nint exp, ptr<floatInfo> _addr_flt) {
    ref floatInfo flt = ref _addr_flt.val;

    if (mant == 0) {
        exp = 0;
    }
    mant<<=60 - flt.mantbits;
    while (mant != 0 && mant & (1 << 60) == 0) {
        mant<<=1;
        exp--;
    } 

    // Round if requested.
    if (prec >= 0 && prec < 15) {
        var shift = uint(prec * 4);
        var extra = (mant << (int)(shift)) & (1 << 60 - 1);
        mant>>=60 - shift;
        if (extra | (mant & 1) > 1 << 59) {
            mant++;
        }
        mant<<=60 - shift;
        if (mant & (1 << 61) != 0) { 
            // Wrapped around.
            mant>>=1;
            exp++;
        }
    }
    var hex = lowerhex;
    if (fmt == 'X') {
        hex = upperhex;
    }
    if (neg) {
        dst = append(dst, '-');
    }
    dst = append(dst, '0', fmt, '0' + byte((mant >> 60) & 1)); 

    // .fraction
    mant<<=4; // remove leading 0 or 1
    if (prec < 0 && mant != 0) {
        dst = append(dst, '.');
        while (mant != 0) {
            dst = append(dst, hex[(mant >> 60) & 15]);
            mant<<=4;
        }
    }
    else if (prec > 0) {
        dst = append(dst, '.');
        for (nint i = 0; i < prec; i++) {
            dst = append(dst, hex[(mant >> 60) & 15]);
            mant<<=4;
        }
    }
    var ch = byte('P');
    if (fmt == lower(fmt)) {
        ch = 'p';
    }
    dst = append(dst, ch);
    if (exp < 0) {
        ch = '-';
        exp = -exp;
    }
    else
 {
        ch = '+';
    }
    dst = append(dst, ch); 

    // dd or ddd or dddd

    if (exp < 100) 
        dst = append(dst, byte(exp / 10) + '0', byte(exp % 10) + '0');
    else if (exp < 1000) 
        dst = append(dst, byte(exp / 100) + '0', byte((exp / 10) % 10) + '0', byte(exp % 10) + '0');
    else 
        dst = append(dst, byte(exp / 1000) + '0', byte(exp / 100) % 10 + '0', byte((exp / 10) % 10) + '0', byte(exp % 10) + '0');
        return dst;
}

private static nint min(nint a, nint b) {
    if (a < b) {
        return a;
    }
    return b;
}

private static nint max(nint a, nint b) {
    if (a > b) {
        return a;
    }
    return b;
}

} // end strconv_package
